import requests
import re
from bs4 import BeautifulSoup
import pandas as pd
from openpyxl import load_workbook
import datetime
import time
import numpy as np
import glob
import numpy as np
import xlwings
import matplotlib.pyplot as plt
def xlsx_broken(file):
"Fonction pour réparer les fichiers excels endommagés et faciliter leur ouverture dans le Jupyter Notebook"
excel_app = xlwings.App(visible=False)
excel_book = excel_app.books.open(file)
excel_book.save()
excel_book.close()
excel_app.quit()
return
url = 'https://www.vendeeglobe.org/fr/classement/20201109_110000'
r = requests.get(url)
content = r.content.decode('UTF-8')
soup = BeautifulSoup(content)
dates = soup.find('select').text
dates = dates.replace('-', '')
dates = dates.replace(':', '')
dates = re.findall('\d+', dates)
years, hours = dates[::2], dates[1::2]
df = pd.DataFrame()
df['years'] = str(years)
df['hours'] = str(hours)
df.sort_values(['years', 'hours'])
df['links'] = 'https://www.vendeeglobe.org/download-race-data/vendeeglobe_'+df['years']+'_'+df['hours']+'.xlsx'
# Webscrapping de l'ensemble des classements du Vendée Globe 2021 et stockage en local
#for i in range(len(df)):
# filename = 'vendeeglobe_'+df['years'][i]+'_'+df['hours'][i]+'.xlsx'
# url = df['links'][i]
# r = requests.get(url)
# open(filename, 'wb').write(r.content)
# file = 'vendeeglobe_20201109_110000.xlsx'
# xlsx_broken(filename)
#def vendeeglobe():
# df = pd.DataFrame()
# files = glob.glob('excels/*.xlsx')
# files.sort()
#
# for i in files:
# csv = pd.read_excel('/Users/pauldm/Documents/TELECOM_Paris/Cours/INFMDI721_Kit Big Data/Projet_final/'+i)
# csv['Date'] = re.findall('\d+', i)[0] + re.findall('\d+', i)[1]
# df = df.append(csv, ignore_index=True)
# return df
#df = vendeeglobe()
#df.to_csv(r'/Users/pauldm/Documents/TELECOM_Paris/Cours/vendeeglobe.csv', index=None, header=True, encoding='utf-8')
#Initialisation du df
df = pd.read_csv('vendeeglobe.csv', sep=',')
# Drop de la colonne 'Unnamed: 0' qui ne contient aucune valeur
df = df.drop(['Unnamed: 0'], axis=1)
# Passage de la colone 'Date' au format date
df['Date'] = pd.to_datetime(df['Date'].values, format='%Y%m%d%H%M%S', errors='ignore')
df = df.set_index('Date')
# Enlever les lignes qui sont des titres
df = df.loc[df['Unnamed: 2']!='Nat. / Voile\nNat. / Sail']
# Enlever les lignes qui n'ont que des NaN
df = df.dropna(axis=0, how='all')
# Enlever les lignes des skippers qui ont abandonné
skipabandon = df.loc[df['Unnamed: 1']=='RET']
df = df[df['Unnamed: 1']!='RET']
df = df[df['Unnamed: 1']!='NL']
# Enlever les lignes qui ne contiennent que du texte
df = df[df['Unnamed: 1']!='Traitements et calculs : Géovoile, un service Hauwell Studios']
df = df[df['Unnamed: 1']!='VMG : Velocity Made Good = projection du vecteur vitesse sur la route théorique. Ou plus simplement : vitesse de rapprochement au but.']
df = df[df['Unnamed: 1']!="DTF : Distance To Finish = Distance théorique la plus courte pour rejoindre l'arrivée; DTL : Distance To Leader = différence de distance au but avec le premier au classement"]
# Enlever les lignes de 'titre' de colonne excel
df = df[df['Unnamed: 1'].notna()]
df = df[df['Unnamed: 1'].str[0]!='C']
# Split du df en deux df : un avec le classement de la course en cours et le
dfc = df[df['Unnamed: 1'].str[-3:]!='ARV']
classement = df[df.index == '2021-03-05 08:00:00']
# Renommer les colonnes
dfc.columns = ['Rank', 'Nationality', 'Skipper',
'Hour_fr', 'Lat_dms', 'Long_dms',
'Cap30_deg', 'Speed30_kts', 'VMG30_kts', 'Distance30_nm',
'CapL_deg', 'SpeedL_kts', 'VMGL_kts', 'DistanceL_nm',
'Cap24h_deg', 'Speed24h_kts', 'VMG24h_kts', 'Distance24h_nm',
'DTF_nm', 'DTL_nm']
# Définir toutes les valeurs en type string pour faciliter les opérations de split
dfc= dfc.astype(str)
# Séparer la nationalité et le sail, ainsi que le crew et le nom du skipper
dfc['Sail'] = dfc['Nationality'].apply(lambda x: x.split()[-1])
dfc['Nationality'] = dfc['Nationality'].apply(lambda x: x.split()[0])
dfc['Crew'] = dfc['Skipper'].apply(lambda x: x.split('\n')[-1])
dfc['Skipper'] = dfc['Skipper'].apply(lambda x: x.split('\n')[0])
# Enlever la partie FR dans l'heure
dfc['Hour_fr'] = dfc['Hour_fr'].apply(lambda x: x.split()[0])
# Enlever les unités de toutes ces mesures
dfc['Speed30_kts'] = dfc['Speed30_kts'].apply(lambda x: x.split()[0])
dfc['Speed24h_kts'] = dfc['Speed24h_kts'].apply(lambda x: x.split()[0])
dfc['SpeedL_kts'] = dfc['SpeedL_kts'].apply(lambda x: x.split()[0])
dfc['VMG30_kts'] = dfc['VMG30_kts'].apply(lambda x: x.split()[0])
dfc['VMGL_kts'] = dfc['VMGL_kts'].apply(lambda x: x.split()[0])
dfc['VMG24h_kts'] = dfc['VMG24h_kts'].apply(lambda x: x.split()[0])
dfc['Distance30_nm'] = dfc['Distance30_nm'].apply(lambda x: x.split()[0])
dfc['DistanceL_nm'] = dfc['DistanceL_nm'].apply(lambda x: x.split()[0])
dfc['Distance24h_nm'] = dfc['Distance24h_nm'].apply(lambda x: x.split()[0])
dfc['DTF_nm'] = dfc['DTF_nm'].apply(lambda x: x.split()[0])
dfc['DTL_nm'] = dfc['DTL_nm'].apply(lambda x: x.split()[0])
dfc['Cap30_deg'] = dfc['Cap30_deg'].str[:-1]
dfc['CapL_deg'] = dfc['Cap30_deg'].str[:-1]
dfc['Cap24h_deg'] = dfc['Cap30_deg'].str[:-1]
# Définition d'une fonction pour convertir les latitudes DMS (degré, min, sec) et DD (decimal degrees)
def dms2dd(dms):
cardinal = dms[-1]
liste = re.findall('\d+', dms)
if cardinal[-1] == 'N' or cardinal[-1] == 'E':
d = round(int(liste[0])+int(liste[1])*(1/60)+int(liste[2])*(1/3600), 4)
else:
d = -round(int(liste[0])+int(liste[1])*(1/60)+int(liste[2])*(1/3600), 4)
return d
dfc['Lat_dms'] = dfc['Lat_dms'].apply(lambda x: dms2dd(x))
dfc['Long_dms'] = dfc['Long_dms'].apply(lambda x: dms2dd(x))
# Réordonner les colonnes pour plus de cohérence
dfc = dfc[['Rank', 'Nationality', 'Crew', 'Skipper', 'Sail', 'Hour_fr', 'Lat_dms', 'Long_dms',
'Cap30_deg', 'Speed30_kts', 'VMG30_kts', 'Distance30_nm', 'CapL_deg',
'SpeedL_kts', 'VMGL_kts', 'DistanceL_nm', 'Cap24h_deg', 'Speed24h_kts',
'VMG24h_kts', 'Distance24h_nm', 'DTF_nm', 'DTL_nm']]
# Enlever les colonnes vides
classement = classement.dropna(axis=1, how='all')
# Supprimer la colonne qui est déjà présente dans le dfc
classement = classement.drop(labels='Unnamed: 2', axis=1)
# Renommer les colonnes
classement.columns = ['Rank_final', 'Skipper', 'Arrival_date', 'Race_time',
'Gap2first', 'Gap2prev', 'OOspeed','OOdistance', '%',
'OGspeed', 'OGdistance']
# Supprimer les unités
classement['Rank_final'] = classement['Rank_final'].apply(lambda x: int(x.split()[0]))
classement['Skipper'] = classement['Skipper'].apply(lambda x: x.split('\n')[0])
classement['OOspeed'] = classement['OOspeed'].apply(lambda x: x.split()[0])
classement['OOdistance'] = classement['OOdistance'].apply(lambda x: x.split()[0])
classement['OGspeed'] = classement['OGspeed'].apply(lambda x: x.split()[0])
classement['OGdistance'] = classement['OGdistance'].apply(lambda x: x.split()[0])
# Affichage
classement.head(2)
| Rank_final | Skipper | Arrival_date | Race_time | Gap2first | Gap2prev | OOspeed | OOdistance | % | OGspeed | OGdistance | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | |||||||||||
| 2021-03-05 08:00:00 | 1 | Yannick Bestaven | 28/01/2021 04:19:46 FR | 80j 03h 44min 46s\n-10h 15min 00s | NaN | NaN | 12.6 | 24365.7 | 117.3 % | 14.8 | 28583.8 |
| 2021-03-05 08:00:00 | 2 | Charlie Dalin | 27/01/2021 20:35:47 FR | 80j 06h 15min 47s\n | 02h 31min 01s | 02h 31min 01s | 12.6 | 24365.7 | 119.6 % | 15.1 | 29135.0 |
dfc = dfc.reset_index()
dfc = dfc.merge(classement, how='left', on='Skipper')
dfc = dfc.fillna(value=0, axis=1)
dfc = dfc.replace({'Rank_final':0}, {'Rank_final':26})
dfc.head(2)
| Date | Rank | Nationality | Crew | Skipper | Sail | Hour_fr | Lat_dms | Long_dms | Cap30_deg | ... | Rank_final | Arrival_date | Race_time | Gap2first | Gap2prev | OOspeed | OOdistance | % | OGspeed | OGdistance | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2020-11-08 14:00:00 | 1 | FRA | Bureau Vallée 2 | Louis Burton | 18 | 15:30 | 46.4128 | -1.8467 | 241 | ... | 3.0 | 28/01/2021 00:45:12 FR | 80j 10h 25min 12s\n | 06h 40min 26s | 04h 09min 25s | 12.6 | 24365.7 | 117.6 % | 14.8 | 28650.0 |
| 1 | 2020-11-08 14:00:00 | 2 | MON | Seaexplorer - Yacht Club De Monaco | Boris Herrmann | 10 | 15:31 | 46.4094 | -1.8394 | 241 | ... | 5.0 | 28/01/2021 11:19:45 FR | 80j 14h 59min 45s\n-06h 00min 00s | 11h 14min 59s | 01h 14min 50s | 12.6 | 24365.7 | 116.8 % | 14.7 | 28448.5 |
2 rows × 33 columns
# Changer le type de l'ensemble des colonnes
dic = {"Rank": int, 'Nationality': str, 'Crew': str, 'Skipper': str, 'Sail': int,
'Cap30_deg': float, 'Speed30_kts': float, 'VMG30_kts': float, 'Distance30_nm': float, 'CapL_deg': float,
'SpeedL_kts': float, 'VMGL_kts': float, 'DistanceL_nm': float, 'Cap24h_deg': float, 'Speed24h_kts': float,
'VMG24h_kts': float, 'Distance24h_nm': float, 'DTF_nm': float, 'DTL_nm': float, 'Rank_final':int,
'OOspeed':float, 'OOdistance':float, 'OGspeed':float, 'OGdistance':float}
dfc = dfc.astype(dic, errors='ignore')
Explication pour chaque colonne :
indice 30 = moyenne sur les 30 dernières minutes indice L = moyenne depuis le dernier classement (toutes les 4 heures à peu près) indice 24h = moyenne sur les dernières 24h
Conversion : Vnoeuds = 1,85185 km/h Dnoeuds = 185185 m
Story telling : Visualisons le parcours des skippers et attardons nous à l'étude du parcours des skippers ayant abandonnés la course.
# Visualisation des parcours du vendée globe 2020 des skipper qui n'ont pas terminé
import plotly.express as px
fig = px.line_geo(dfc,
lat=dfc['Lat_dms'],
lon=dfc['Long_dms'],
projection="orthographic",
color='Skipper',
hover_name='Date',
title= "Parcours de l'ensemble des skippers")
fig.show()
dfabandon = dfc.loc[dfc['Rank_final']==26]
#dfabandon = dfabandon.sort_values("Date").groupby(["Skipper"]).last().drop(['Sébastien Destremau',
#'Isabelle Joschke',
#'Nicolas Troussel'], axis=0).reset_index()
# Visualisation des parcours du vendée globe 2020 des skipper qui ont abandonné
import plotly.express as px
fig = px.line_geo(dfabandon,
lat=dfabandon['Lat_dms'],
lon=dfabandon['Long_dms'],
color='Skipper',
hover_name='Date',
projection="orthographic",
title='Parcours des 8 skippers ayant abandonnés la course')
fig.show()
Story telling :
On observe que sur les 8 skippers qui ne sont pas allés au bout de la course, 5 ont abandonné à peu de chose près au niveau des mêmes latitudes, proche des côtes de l'Afrique du Sud. Simple coïncidence ou zone difficile du parcours ?
Voici les raisons des abandons :
Alex thomson : safran endommagé
Fabrice Amedeo : problème d'ordinateur de bord
Kevin Escoffier : voie d'eau
Samatha Davies : collision et dégat sur la quille
Sébastien Simon : collision et dégat sur le foil
D'après mes recherches tous les skippers sauf Kevin Escoffier semblait confiant sur leur capacité à poursuivre la course si il s'agissait d'une zone moins périlleuse. En effet, ils allaient entamer la partie la plus ardue du Vendée Globe avec l'Antarctique et le Cap Horn au Sud de l'Amérique du Sud.
Nous pouvons conclure que ce n'est pas une coincidence si les skippers ont abandonné aux abords du Cap de Bonne Espérance.
# Garder une seule ligne de donnée par jour pour l'ensemble des skippers (celle de 21h)
df24h = dfc.copy()
df24h['Hour'] = df24h['Date'].apply(lambda x: x.hour)
df24h['Date'] = df24h['Date'].apply(lambda x: x.date())
df24h = df24h.sort_values("Hour").groupby(["Skipper","Date"]).last()
df24h = df24h.reset_index()
df24h = df24h.loc[df24h['Rank_final']!=26]
dfpivot = df24h.pivot_table(values=['Speed24h_kts', 'Distance24h_nm'],
index=['Skipper', 'Rank_final'],
aggfunc={'Speed24h_kts':'mean','Distance24h_nm':['sum', 'mean']}).reset_index()
dfpivot['Rank_final'] = dfpivot['Rank_final'].astype(int)
dfpivot.sort_values('Rank_final', inplace=True)
dfpivot.columns = ['Skipper', 'Rank_final', 'Distance24h_nm.mean', 'Distance24h_nm.sum', 'Speed24h_kts.mean']
dfpivot['Speed24h_kts.mean'] = dfpivot['Speed24h_kts.mean'].round(2)
fig = px.bar(dfpivot,
x='Skipper',
y='Speed24h_kts.mean',
color='Rank_final',
text='Speed24h_kts.mean',
labels={'Skipper':'Skippers', 'Speed24h_kts.mean':'Vitesse moyenne'},
title='La vitesse est-elle déterminante pour gagner la course ?',
range_y=[8, 15.5])
fig.show()
tmp = dfc.loc[dfc['Date']=='2020-11-08 14:00:00']
tmp = tmp.loc[tmp['OOspeed']!=0]
tmp.sort_values('Rank_final', inplace=True)
fig = px.bar(tmp,
x='Skipper',
y='OOspeed',
color='Rank_final',
text='OOspeed',
labels={'Skipper':'Skippers', 'OOspeed':"Vitesse sur l'ortho"},
range_y=[8, 15.5])
fig.show()
fig = px.bar(tmp,
x='Skipper',
y='OGspeed',
color='Rank_final',
text='OGspeed',
labels={'Skipper':'Skippers', 'OGspeed':'Vitesse sur le fond'},
range_y=[8, 15.5])
fig.show()
Story telling : Nous étudions ici 3 vitesses différentes. Rapellons le sens de chacune d'elle.
Le premier graphe nous donne une tendance qui semble logique : vitesse et classement final sont corrélés. Le deuxième graphe confirme cela et montre que les marins qui vont le plus vite sur l'eau sont ceux qui arrivent en premier. Le troisième graphe est très intéressant, il montre que certains marins ont une vitesse sur le fond plus importante mais que cela n'implique pas forcément une meilleure position au classement. Cela siginifie qu'ils ont plus souvent bénéficiés de courant en leur faveur.
Etudions le trajet de Thomas Ruyant et Jean le Cam pour observer ces différences. Thomas Ruyant a une vitesse sur le fond plus élévé mais pourtant il est arrivé après Jean Le Cam. Nous nous attendons donc a observer que Thomas Ruyant a privilégié la stratégie suivante : suivre les courants quitte à faire un trajet plus long.
tmp = df24h.loc[df24h['Skipper'].isin(['Jean Le Cam', 'Thomas Ruyant'])]
fig = px.line_geo(tmp,
lat='Lat_dms',
lon='Long_dms',
color='Skipper',
projection='robinson')
fig.show()
print('Jean le Cam : distance sur le fond = {}\nThomas Ruyant : distance sur le fond = {}'.format(tmp['OGdistance'].unique()[0],
tmp['OGdistance'].unique()[1]))
Jean le Cam : distance sur le fond = 27501.5 Thomas Ruyant : distance sur le fond = 29175.5
Thomas Ruyant a en effet parcouru un distance sur le fond bien plus importante que Jean le Cam, environ 1674 nm.
Calculons maintenant leurs temps de course respectifs. Pour cela, rapellons leurs vitesses moyenne sur le fond respective :
JleCam = str(datetime.timedelta(hours=tmp['OGdistance'].unique()[0]/14.1))
TRuyant= str(datetime.timedelta(hours=tmp['OGdistance'].unique()[1]/15.1))
print("Le temps de course de Jean le Cam est {}\nLe temps de course de Thomas Ruyant est {}".format(JleCam, TRuyant))
Le temps de course de Jean le Cam est 81 days, 6:27:39.574468 Le temps de course de Thomas Ruyant est 80 days, 12:09:08.344371
Au vue de ces résultats une question se pose, comment Jean Le Cam peut-il prétendre à la 3ème place si il a mis plus de temps que Thomas Ruyant (5ème) pour terminer la course?
(Après recherche sur internet) Jean Le Cam a fait un détour pour porter secours à un autre skipper, Kevin Escoffier, et il a donc bénéficié de 16h et 15 minutes de compensation. Cela explique pourquoi Jean le Cam s'est finalement placé en 3ème position.
tmp = dfc.loc[dfc['Skipper'].isin(['Jean Le Cam', 'Kevin Escoffier'])]
fig = px.line_geo(tmp,
lat='Lat_dms',
lon='Long_dms',
color='Skipper',
projection='azimuthal equal area')
fig.show()
Sur cette représentation il est possible de voir que les parcours de Kevin Escoffier et Jean Le Cam se retrouve à la position : lat =-40,95, long=9,27.
Ces premières observations post-traitement des données permettent d'identifier des éléments de course invisible à vue d'oeil. Une étude plus approfondie avec des algorithmes de clustering, classification pourrait mettre en relief d'autres comportements et caractéristiques de la course du Vendée Globe.